/**************************************************************
 * lyricparse.c - 
 * Copyright (c) 2009  Qin Xiaoxue  <crs.chin@gmail.com>
 *
 * This file is  free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
**************************************************************/


#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <stdlib.h>
#include <err.h>
#include <assert.h>
#include <unistd.h>

#include "common.h"
#include "lyricparse.h"


#undef  TITLE
#undef  ARTIST
#undef  ALBUM
#undef  AUTHOR
#undef  EDITOR
#undef  PRODUCER
#undef  VERSION
#undef  OFFSET
#undef  TAG_EN

char *tags[]=
  {
    "ti","ar","al","au","by","re","ve","offset"
  };

enum __index {
  TITLE = 0,
  ARTIST,
  ALBUM,
  AUTHOR,
  EDITOR,
  PRODUCER,
  VERSION,
  OFFSET,
  TAG_END
};

/*lyric line orgnized in double linked chain*/
struct lyric_line
{
  /*count in milliseconds*/
  int time;
  char *line;
  struct lyric_line *next;
  struct lyric_line *prev;
};

struct lyric
{
  char *title;
  char *artist;
  char *album;
  char *author;  /*song lyric author*/

  char *editor;    /*person who edited this lyric file*/
  char *producer;   /*program name that create the lyric file*/
  char *version;   /*version of the program that create the lyric file*/

  float offset;

  int valid;  /*if already parsed*/

  struct lyric_line *lines;
};


static struct lyric_line *get_lyric_line()
{
  struct lyric_line *l;

  l=(struct lyric_line *)malloc(sizeof(struct lyric_line));
  if(!l) {
    fprintf(stderr,"**OOM! aborting\n");
    exit (FALSE);
  }
  memset(l,0,sizeof(struct lyric_line));
  return l;
}


static lyric_t *get_lyric_t()
{
  lyric_t *lp;

  lp=(lyric_t *)malloc(sizeof(lyric_t));
  if(!lp) {
    fprintf(stderr,"** OOM! aborting..\n");
    exit (FALSE);
  }
  memset(lp,0,sizeof(lyric_t));
  return lp;
}


static char *get_tagvalue(FILE *fp)
{
  char t[MAX_TAGLEN+1];
  int i,j;
  char *s;

  for(i=0;(t[i]=getc(fp)) !=']' && i<MAX_TAGLEN;i++)  {
    if(t[i]=='[')  {
      ungetc(t[i],fp);
      fprintf(stderr,"**unexpacted '[' encountered at offset:%d.ignoring...\n",ftell(fp));
      return NULL;
    }
    if(feof(fp))
      fprintf(stderr,"**lyric file corrupted or malformat! ignoring.\n");
    if(i==MAX_TAGLEN)  {
      fprintf(stderr,"**too long of the tag value,malformat of the lyric file maybe.ignoring..\n");
      while((getc(fp)) != ']');
      break;
    }
  }

  t[i]='\0';
  strip(t,FALSE);
  if(strlen(t)==0)
    return NULL;
  s=(char *)malloc(strlen(t)+1);
  if(!s) {
    fprintf(stderr,"**running out of memory!Aborting...");
    exit (FALSE);
  }
  strcpy(s,t);
  return s;
}


static void skip_invalid_marker(char *maker,FILE *fp)
{
  char c;

  fprintf(stderr,"**invalid tag name:'[%s",maker);
  while((c=getc(fp)) != ']')
    fputc(c,stderr);
  fprintf(stderr,"]':skipped...");
}


/*return minus for error*/
static int parse_timestamp(char *min,FILE *fp)
{
  int tm = 0;
  int sec = 0;
  int xsec = 0;
  int ret;
  char c;

  ret=sscanf(min,"%d",&tm);
  if(!ret)
    return -1.0;
  if(tm<0)  {
    fprintf(stderr,"**bad time stamp :[%s",min);
    do
      fputc(c=fgetc(fp), stderr);
    while(c!=']');
    putc('\n',stderr);
    return -1;
  }

  ret=fscanf(fp,"%d . %d", &sec, &xsec);
  if( ret < 1  || feof(fp) ) {
    while(getc(fp) != ']');
    return -1;
  }

  do
    c=getc(fp);
  while(isspace(c) || c == ' ');    /* ' ' dont count as space in isspace*/

  if( c != ']' )  {
    while(getc(fp) != ']' && ! feof(fp));
    fprintf(stderr,"** unexpacted cahr before ']' at offset:%d! skipped..\n",ftell(fp));
  }

  return (tm * 60 + sec) * 1000 + xsec * 10;
}

/* FIXME: currently ignore offset value in lyric file  */
static float get_offsetvalue(FILE *fp)
{
  while(getc(fp)!=']');
  return 0.0;
}

/*
 *parse infomation between [ ] in the lyric file
 *if @*timestamp is minus, that is invalid,return
 *TRUE for parseing ok, FALSE when error occurred.
 */
static int marker_parse(FILE *fp, lyric_t *lyricdata, int *timestamp)
{
  char str[10];
  int i;
  char c;
  int istag;

  istag = 0;
  *timestamp = -1;

  for(i = 0; (c = getc(fp)) != ':'; )  {
    if(isspace(c))
      continue;
    str[i++] = c;
    if(c == ']' || i == 8)   {
      fprintf(stderr,"**malformat marker :[%s]. skipped...\n",str);
      return FALSE;
    }
  }
  str[i] = '\0';

  if(isalpha(str[0])) {
    for(i=0; i < TAG_END; i++)
      if(! strcasecmp(str,tags[i]))
		break;
    switch(i) {
    case TITLE:
      if(lyricdata->title)
		free(lyricdata->title);
      lyricdata->title = get_tagvalue(fp);
      if(! lyricdata->title)
		return FALSE;
      break;
    case ARTIST:
      if(lyricdata->artist)
		free(lyricdata->artist);
      lyricdata->artist = get_tagvalue(fp);
      if(! lyricdata->artist)
		return FALSE;
      break;
    case ALBUM:
      if(lyricdata->album)
		free(lyricdata->album);
      lyricdata->album = get_tagvalue(fp);
      if(! lyricdata->album)
		return FALSE;
      break;
    case AUTHOR:
      if(lyricdata->author)
		free(lyricdata->author);
      lyricdata->author = get_tagvalue(fp);
      if(! lyricdata->author)
		return FALSE;
      break;
    case EDITOR:
      if(lyricdata->editor)
		free(lyricdata->editor);
      lyricdata->editor = get_tagvalue(fp);
      if(! lyricdata->editor)
		return FALSE;
      break;
    case PRODUCER:
      if(lyricdata->producer)
		free(lyricdata->producer);
      lyricdata->producer = get_tagvalue(fp);
      if(! lyricdata->producer)
		return FALSE;
      break;
    case VERSION:
      if(lyricdata->version)
		free(lyricdata->version);
      lyricdata->version = get_tagvalue(fp);
      if(! lyricdata->version)
		return FALSE;
      break;
    case OFFSET:
      lyricdata->offset = get_offsetvalue(fp);
      break;
    default:
      skip_invalid_marker(str,fp);
      return FALSE;
      break;
    }
    return TRUE;
  }

  if(isalldigit(str))  {
    *timestamp = parse_timestamp(str, fp);
    if(*timestamp < 0)
      return FALSE;
    return TRUE;
  }

  skip_invalid_marker(str,fp);
  return FALSE;
}


static char *get_lyric_text(FILE *fp)
{
  char *s = NULL;
  char *head = "";
  int i;
  char c;

  for(;;)  {
    s = (char *)malloc(strlen(head) + 30);
    if(!s)  {
      fprintf(stderr,"** OOM! aborting.\n");
      exit (-1);
    }
    if(head != NULL)
      strcpy(s,head);
    head = s;
    s += strlen(s);
    for(i=0; i < (s == head ? 29:30); i++)  {
      c = getc(fp);
      if(feof(fp))
		return strip(head, FALSE);
      if(c == '[')  {
		ungetc(c,fp);
		s[i] = '\0';
		return strip(head, FALSE);
      }
      s[i] = c;
    }
  }
}


/*
 *everything should always be ok when returned , or never return
 *if error occurred.
 */
static void insert_lyric_time(lyric_t *lyricdata, int time)
{
  struct lyric_line  *l, *iterate;

  l = get_lyric_line();
  l->time = time;

  for(iterate = lyricdata->lines; iterate != NULL; iterate = iterate->next)   {
	if(time < iterate->time)  {
	  iterate->prev = l;
	  l->next = iterate;
	  l->prev = NULL;
	  lyricdata->lines = l;
	  return;
	}

    if(time > iterate->time)
	  if(! iterate->next || time < iterate->next->time)
		break;
  }

  if(iterate == NULL)  {
    lyricdata->lines = l;
    return;
  }

  l->next = iterate->next;
  l->prev = iterate;
  iterate->next = l;

  if(! l->next)
    return;
  l->next->prev = l;
}

/*
 * with different copied for some lyric line at defferent time
 * but that dont happens much.
 */
static int insert_lyric_text(lyric_t *lyricdata, char *text)
{
  struct lyric_line *l;
  char *t;
  int cnt = 0;

  for(l = lyricdata->lines ; l != NULL ; l = l->next)
    if(! l->line)  {
	  DBGMSG("INSERTING:%s\n",text);
      if(cnt > 0)  {
		t = (char *)malloc(strlen(text)+1);
		if(! t) {
		  fprintf(stderr,"** run out of memory! aborting");
		  exit (FALSE);
		}
		strcpy(t, text);
		l->line = t;
		continue;
      }
      l->line = text;
      cnt++;
    }

  if(cnt == 0)   {
    fprintf(stderr,"** expected lyric marker before \"%s\".skipped..\n",text);
    free(text);
    return FALSE;
  }
  return TRUE;
}


lyric_t *lyricparse_parse_lyric(FILE *fp)
{
  lyric_t *lyric = NULL;
  char *text = NULL;
  char c;
  int ret;
  int time;
  int isbeginning = 1;  /*skeep texts at the beginning of the lrc file*/

  if(!fp)
    return NULL;
  rewind(fp);
  lyric = get_lyric_t();
  for(;;)  {
    while(getc(fp) != '['  && ! feof(fp));
    if(feof(fp))  {
      if(! lyric->valid)  {
		lyricparse_release_lyric(lyric);
		return NULL;
      }
#ifndef NDEBUG
	  struct lyric_line *l = lyric->lines;
	  while(l)  {
		printf("%d    %s\n", l->time, l->line ?: "(NULL)");
		l = l->next;
	  }
#endif
      return lyric;
    }

	ret = marker_parse(fp, lyric, &time);
	if(ret == TRUE)
	  lyric->valid=TRUE;
	if(time >= 0)  {
	  insert_lyric_time(lyric, time);
	  while(isspace(c = getc(fp)));
		ungetc(c, fp);
	  if(c == '[') 
		continue;
	}else
	  continue;

    text = get_lyric_text(fp);
    if(text)  {
      lyric->valid = TRUE;
      insert_lyric_text(lyric, text);
    }
  }
}


static FILE *checked_fopen(char *path,char *mode)
{
  FILE *fp=NULL;

  fp=fopen(path,mode);
  if(!fp)
    fprintf(stderr,"** can not open file %s .\n",path);
  return fp;
}


lyric_t *lyricparse_parse_lyric_file(char *path)
{
  lyric_t *ret = NULL;
  FILE *fp=checked_fopen(path,"r");
  if(fp) {
    ret = lyricparse_parse_lyric(fp);
    fclose(fp);
  }
  return ret;
}

#define __lyricparse_get_attrib(tag)					\
  const  char *lyricparse_get_##tag(lyric_t *lyricdata)	\
  {														\
    return lyricdata->tag;								\
  }

#define __lyricparse_set_attrib(tag)								\
  void lyricparse_set_##tag(lyric_t *lyricdata, const char *tag)	\
  {																	\
    if(lyricdata->tag)												\
      free(lyricdata->tag);											\
    lyricdata->tag = strdup(tag);									\
  }

__lyricparse_get_attrib(title);
__lyricparse_get_attrib(artist);
__lyricparse_get_attrib(album);
__lyricparse_get_attrib(author);

__lyricparse_get_attrib(editor);
__lyricparse_get_attrib(producer);
__lyricparse_get_attrib(version);


__lyricparse_set_attrib(title);
__lyricparse_set_attrib(artist);
__lyricparse_set_attrib(album);
__lyricparse_set_attrib(author);

__lyricparse_set_attrib(editor);
__lyricparse_set_attrib(producer);
__lyricparse_set_attrib(version);

#undef __get_attrib
#undef __set_attrib



int lyricparse_get_begintime(lyric_t *lyricdata)
{
  if(lyricdata->lines)
    return lyricdata->lines->time;
  return -1;
}


int lyricparse_get_lyric(lyric_t *lyricdata, char **text, int *begin, int *end, int now)
{
  struct lyric_line *l;

  if(! lyricdata || ! text || ! begin || ! end)
	return FALSE;

  l = lyricdata->lines;
  if(now < l->time)  {
	*text = "";
	*begin = 0;
	*end = l->time;
	return TRUE;
  }

  for(; l ; l = l->next)
    if(now >= l->time)  {
      if(l->next == NULL)
		break;
      if(now < l->next->time)
		break;
	}

  if(l == NULL)  {
    *text = NULL;
    return FALSE;
  }

  *text = l->line;
  *begin = l->time;
  if(l->next)
	*end = l->next->time;
  else
    *end = 0;

  return TRUE;
}


/*
 * return TRUE if fetched proper lyric line
 * FLASE for else. @*timeend could be NULL if
 * this lyric line last to the end
 */
/*  abandoned */
/* int get_line(lyric_t *lyricdata, char **text, float **timebegin, float **timeend, float when) */
/* { */
/*   struct lyric_line *l; */

/*   if(when < 0) */
/*     return FALSE; */

/*   for(l=lyricdata->lines; l != NULL ; l=l->next) */
/*     if(when >= l->time) */
/*       if(l->next == NULL) */
/* 	break; */
/*       else if(when < l->next->time) */
/* 	break; */
/*   if(l == NULL)  { */
/*     *text=NULL; */
/*     *timebegin=NULL; */
/*     *timeend=NULL; */
/*     return FALSE; */
/*   } */
/*   *text=l->line; */
/*   *timebegin=&l->time; */
/*   if(! l->next)  { */
/*     *timeend=NULL; */
/*     return TRUE; */
/*   } */
/*   *timeend=&l->next->time; */
/*   return TRUE; */
/* } */


int save_lyric(lyric_t *lyricdata,FILE *fp)
{
  struct lyric_line *l = lyricdata->lines;

  if(!fp)
    return FALSE;
  if(lyricdata->title)
    fprintf(fp,"[%s:%s]\n", tags[TITLE], lyricdata->title);
  if(lyricdata->artist)
    fprintf(fp,"[%s:%s]\n", tags[ARTIST], lyricdata->artist);
  if(lyricdata->album)
    fprintf(fp,"[%s:%s]\n", tags[ALBUM], lyricdata->album);
  if(lyricdata->author)
    fprintf(fp, "[%s:%s]\n", tags[AUTHOR], lyricdata->author);
  if(lyricdata->offset != 0)
    fprintf(fp, "[%s:%+d]\n", tags[OFFSET], lyricdata->offset);

  fprintf(fp, "[%s:%s]\n", tags[PRODUCER], PROG_NAME);
  fprintf(fp, "[%s:%s]\n", tags[VERSION], LIB_VERSION);

  while(l != NULL) {
    fprintf(fp,"[%d:%d.%d]%s\n",l->time / (1000 * 60), (l->time / 1000) % 60,
			(l->time % 1000) / 10, l->line);
    l=l->next;
  }
  return TRUE;

}


int lyricparse_save_lyric_file(lyric_t *lyricdata, char *path)
{
  int ret;
  FILE *fp = checked_fopen(path,"w");

  if(fp) {
    ret = save_lyric(lyricdata,fp);
    fclose(fp);
  }
  return ret;
}


int lyricparse_save_as_srt(lyric_t *lyricdata, FILE *fp)
{
  struct lyric_line *l=lyricdata->lines;
  int i=1;

  while(l != NULL && l->next != NULL) {
    fprintf(fp,"%d\n%d:%d:%d,%d --> %d:%d:%d,%d\n%s\n\n", i++,
			(l->time + 1) / (3600 * 1000), (l->time + 1) / 1000 % 3600 / 60,
			(l->time + 1) / 1000 % 3600 % 60, (l->time + 1) % 1000,
			l->next->time / (3600 * 1000), l->next->time / 1000 % 3600 / 60,
			l->next->time / 1000 %3600 % 60, l->next->time % 1000,
			l->line);
    l = l->next;
  }

  /*
   *cause lrc don't specify the end time of the music
   *so we don't know the end time of the last line of srt
   *file, so we just add 5 seconds.
   */
    fprintf(fp,"%d\n%d:%d:%d,%d --> %d:%d:%d,%d\n%s\n\n", i++,
			(l->time + 1) / (3600 * 1000), (l->time + 1) / 1000 % 3600 / 60,
			(l->time + 1) / 1000 % 3600 % 60, (l->time + 1) % 1000,
			(l->time + 1) / (3600 * 1000), (l->time + 1) / 1000 % 3600 / 60,
			(l->time + 1) / 1000 % 3600 % 60 + 5, (l->time + 1) % 1000,
			l->line);

  return TRUE;
}


int lyricparse_save_as_srt_file(lyric_t *lyricdata,char *path)
{
  int ret=0;
  FILE *fp=checked_fopen(path,"w");
  if(fp) {
    ret = lyricparse_save_as_srt(lyricdata,fp);
    fclose(fp);
  }
  return ret;
}


/* FIXME: not yes implemented */
int lyricparse_write_meta_info(lyric_t *lyricdata,char *path)
{
  return TRUE;
}


void lyricparse_release_lyric(lyric_t *lyricdata)
{
  struct lyric_line  *l,*t;

  l = lyricdata->lines;
  while(l != NULL)  {
    if(l->line)
      free(l->line);
    t = l;
    l = l->next;
    free(t);
  }

#define __FREE(attrib)				\
  if(lyricdata->attrib)				\
    free(lyricdata->attrib)

  __FREE(title);
  __FREE(artist);
  __FREE(album);
  __FREE(author);

  __FREE(editor);
  __FREE(producer);
  __FREE(version);
#undef __FREE

  free(lyricdata);
}

////////////////////////////////////////////////////////
//////////////// testing part //////////////////////////
////////////////////////////////////////////////////////

#ifdef TEST_LYRIC

void test_lyric(lyric_t *lyricdata)
{
#define __print_tagvalue(tag)			\
  if(lyricdata->tag)				\
    printf(#tag ": %s\n",lyricdata->tag)

  __print_tagvalue(artist);
  __print_tagvalue(title);
  __print_tagvalue(album);
  __print_tagvalue(author);
  __print_tagvalue(editor);
  __print_tagvalue(producer);
  __print_tagvalue(version);

#undef __print_tagvalue

  struct lyric_line *l=lyricdata->lines;
  while(l != NULL)  {
    printf("%f :%s \n",l->time,l->line);
    l=l->next;
  }
}


int main(int argc,char **argv)
{
  int i;
  char *text;
  int time, begin, end;
  lyric_t *l = lyricparse_parse_lyric_file(argv[1]);


  printf("==========================================================\n");
  if(l != NULL) {
    time = lyricparse_get_begintime(l);
    while(1)  {
      i=lyricparse_get_lyric(l, &text, &begin, &end, time);
      if(i == FALSE)  
		break;
      if(end == 0)  {
		printf("%d -> -- :%s \n", begin, text);
		break;
      }
      printf("%d -> %d :%s \n", begin, end, text);
      time = end;
    }
  }
 
  if(l) {
    lyricparse_save_lyric_file(l,"yyyy.lrc");
    lyricparse_save_as_srt_file(l,"xxxx.srt");
    lyricparse_release_lyric(l);
  }
  return 0;
}

#endif
